/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.solr.search;

import java.lang.invoke.MethodHandles;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.apache.lucene.tests.util.TestUtil;
import org.apache.solr.client.solrj.request.CollectionAdminRequest;
import org.apache.solr.client.solrj.request.SolrQuery;
import org.apache.solr.client.solrj.request.UpdateRequest;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.client.solrj.response.RangeFacet;
import org.apache.solr.cloud.SolrCloudTestCase;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.params.CoreAdminParams;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.schema.CurrencyFieldTypeTest;
import org.junit.BeforeClass;
import org.junit.Test;

public class CurrencyRangeFacetCloudTest extends SolrCloudTestCase {

  private static final String COLLECTION = MethodHandles.lookup().lookupClass().getName();
  private static final String CONF = COLLECTION + "_configSet";

  private static String FIELD = "amount";

  private static final List<String> STR_VALS = Arrays.asList("x0", "x1", "x2");
  // NOTE: in our test conversions EUR uses an asymmetric exchange rate
  // these are the equivalent values relative to: USD EUR GBP
  private static final List<String> VALUES =
      Arrays.asList(
          "10.00,USD", // 10.00,USD  25.00,EUR   5.00,GBP
          "15.00,EUR", //  7.50,USD  15.00,EUR   7.50,GBP
          "6.00,GBP", // 12.00,USD  12.00,EUR   6.00,GBP
          "7.00,EUR", //  3.50,USD   7.00,EUR   3.50,GBP
          "2,GBP"); //  4.00,USD   4.00,EUR   2.00,GBP
  private static final int NUM_DOCS = STR_VALS.size() * VALUES.size();

  @BeforeClass
  public static void setupCluster() throws Exception {
    CurrencyFieldTypeTest.assumeCurrencySupport("USD", "EUR", "MXN", "GBP", "JPY", "NOK");

    final int numShards = TestUtil.nextInt(random(), 1, 5);
    final int numReplicas = 1;
    final int nodeCount = numShards * numReplicas;

    configureCluster(nodeCount)
        .addConfig(CONF, TEST_HOME().resolve("collection1").resolve("conf"))
        .configure();

    assertEquals(
        0,
        (CollectionAdminRequest.createCollection(COLLECTION, CONF, numShards, numReplicas)
                .setProperties(
                    Collections.singletonMap(CoreAdminParams.CONFIG, "solrconfig-minimal.xml"))
                .process(cluster.getSolrClient()))
            .getStatus());

    // we're indexing each Currency value in 3 docs, each with a diff 'x_s' field value
    // use modulo to pick the values, so we don't add the docs in strict order of either VALUES of
    // STR_VALS (that way if we want to filter by id later, it's an independent variable)
    for (int id = 0; id < NUM_DOCS; id++) {
      final String x = STR_VALS.get(id % STR_VALS.size());
      final String val = VALUES.get(id % VALUES.size());
      assertEquals(
          0,
          (new UpdateRequest().add(sdoc("id", "" + id, "x_s", x, FIELD, val)))
              .process(cluster.getSolrClient(COLLECTION))
              .getStatus());
    }
    assertEquals(0, cluster.getSolrClient(COLLECTION).commit().getStatus());
  }

  public void testSimpleRangeFacetsOfSymmetricRates() throws Exception {
    for (boolean use_mincount : Arrays.asList(true, false)) {

      // exchange rates relative to USD...
      //
      // for all of these permutations, the numDocs in each bucket that we get back should be the
      // same (regardless of any asymmetric exchanges ranges, or the currency used for the
      // 'gap')
      // because the start & end are always in USD.
      //
      // NOTE:
      //  - 0,1,2 are the *input* start,gap,end
      //  - 3,4,5 are the *normalized* start,gap,end expected in the response
      for (List<String> args :
          Arrays.asList( // default currency is USD
              Arrays.asList("4", "1.00", "11.0", "4.00,USD", "1.00,USD", "11.00,USD"),
              // explicit USD
              Arrays.asList("4,USD", "1,USD", "11,USD", "4.00,USD", "1.00,USD", "11.00,USD"),
              // Gap can be in diff currency (but start/end must currently match)
              Arrays.asList("4.00,USD", "000.50,GBP", "11,USD", "4.00,USD", ".50,GBP", "11.00,USD"),
              Arrays.asList("4.00,USD", "2,EUR", "11,USD", "4.00,USD", "2.00,EUR", "11.00,USD"))) {

        assertEquals(6, args.size()); // sanity check

        // first let's check facet.range
        SolrQuery solrQuery =
            new SolrQuery(
                "q",
                "*:*",
                "rows",
                "0",
                "facet",
                "true",
                "facet.range",
                FIELD,
                "facet.mincount",
                (use_mincount ? "3" : "0"),
                "f." + FIELD + ".facet.range.start",
                args.get(0),
                "f." + FIELD + ".facet.range.gap",
                args.get(1),
                "f." + FIELD + ".facet.range.end",
                args.get(2),
                "f." + FIELD + ".facet.range.other",
                "all");
        QueryResponse rsp = cluster.getSolrClient(COLLECTION).query(solrQuery);
        try {
          assertEquals(NUM_DOCS, rsp.getResults().getNumFound());

          final String start = args.get(3);
          final String gap = args.get(4);
          final String end = args.get(5);

          @SuppressWarnings({"rawtypes"})
          final List<RangeFacet> range_facets = rsp.getFacetRanges();
          assertEquals(1, range_facets.size());
          @SuppressWarnings({"rawtypes"})
          final RangeFacet result = range_facets.get(0);
          assertEquals(FIELD, result.getName());
          assertEquals(start, result.getStart());
          assertEquals(gap, result.getGap());
          assertEquals(end, result.getEnd());
          assertEquals(3, result.getBefore());
          assertEquals(3, result.getAfter());
          assertEquals(9, result.getBetween());

          @SuppressWarnings({"unchecked"})
          List<RangeFacet.Count> counts = result.getCounts();
          if (use_mincount) {
            assertEquals(3, counts.size());
            for (int i = 0; i < 3; i++) {
              RangeFacet.Count bucket = counts.get(i);
              assertEquals((4 + (i * 3)) + ".00,USD", bucket.getValue());
              assertEquals("bucket #" + i, 3, bucket.getCount());
            }
          } else {
            assertEquals(7, counts.size());
            for (int i = 0; i < 7; i++) {
              RangeFacet.Count bucket = counts.get(i);
              assertEquals((4 + i) + ".00,USD", bucket.getValue());
              assertEquals("bucket #" + i, (i == 0 || i == 3 || i == 6) ? 3 : 0, bucket.getCount());
            }
          }
        } catch (AssertionError | RuntimeException ae) {
          throw new AssertionError(solrQuery + " -> " + rsp + " ===> " + ae.getMessage(), ae);
        }

        // same basic logic, w/json.facet
        solrQuery =
            new SolrQuery(
                "q",
                "*:*",
                "rows",
                "0",
                "json.facet",
                "{ foo:{ type:range, field:"
                    + FIELD
                    + ", mincount:"
                    + (use_mincount ? 3 : 0)
                    + ", "
                    + "        start:'"
                    + args.get(0)
                    + "', gap:'"
                    + args.get(1)
                    + "', end:'"
                    + args.get(2)
                    + "', other:all}}");
        rsp = cluster.getSolrClient(COLLECTION).query(solrQuery);
        try {
          assertEquals(NUM_DOCS, rsp.getResults().getNumFound());

          @SuppressWarnings({"unchecked"})
          final NamedList<Object> foo =
              ((NamedList<NamedList<Object>>) rsp.getResponse().get("facets")).get("foo");

          assertEquals("before", 3L, ((NamedList) foo.get("before")).get("count"));
          assertEquals("after", 3L, ((NamedList) foo.get("after")).get("count"));
          assertEquals("between", 9L, ((NamedList) foo.get("between")).get("count"));

          @SuppressWarnings({"unchecked", "rawtypes"})
          final List<NamedList> buckets = (List<NamedList>) foo.get("buckets");

          if (use_mincount) {
            assertEquals(3, buckets.size());
            for (int i = 0; i < 3; i++) {
              @SuppressWarnings({"rawtypes"})
              NamedList bucket = buckets.get(i);
              assertEquals((4 + (3 * i)) + ".00,USD", bucket.get("val"));
              assertEquals("bucket #" + i, 3L, bucket.get("count"));
            }
          } else {
            assertEquals(7, buckets.size());
            for (int i = 0; i < 7; i++) {
              @SuppressWarnings({"rawtypes"})
              NamedList bucket = buckets.get(i);
              assertEquals((4 + i) + ".00,USD", bucket.get("val"));
              assertEquals(
                  "bucket #" + i, (i == 0 || i == 3 || i == 6) ? 3L : 0L, bucket.get("count"));
            }
          }
        } catch (AssertionError | RuntimeException ae) {
          throw new AssertionError(solrQuery + " -> " + rsp + " ===> " + ae.getMessage(), ae);
        }
      }
    }
  }

  public void testFacetRangeOfAsymmetricRates() throws Exception {
    // facet.range: exchange rates relative to EUR...
    //
    // because of the asymmetric exchange rate, the counts for these buckets will be different
    // from if we just converted the EUR values to USD
    for (boolean use_mincount : Arrays.asList(true, false)) {
      final SolrQuery solrQuery =
          new SolrQuery(
              "q",
              "*:*",
              "rows",
              "0",
              "facet",
              "true",
              "facet.range",
              FIELD,
              "facet.mincount",
              (use_mincount ? "3" : "0"),
              "f." + FIELD + ".facet.range.start",
              "8,EUR",
              "f." + FIELD + ".facet.range.gap",
              "2,EUR",
              "f." + FIELD + ".facet.range.end",
              "22,EUR",
              "f." + FIELD + ".facet.range.other",
              "all");
      final QueryResponse rsp = cluster.getSolrClient(COLLECTION).query(solrQuery);
      try {
        assertEquals(NUM_DOCS, rsp.getResults().getNumFound());
        @SuppressWarnings({"rawtypes"})
        final List<RangeFacet> range_facets = rsp.getFacetRanges();
        assertEquals(1, range_facets.size());
        @SuppressWarnings({"rawtypes"})
        final RangeFacet result = range_facets.get(0);
        assertEquals(FIELD, result.getName());
        assertEquals("8.00,EUR", result.getStart());
        assertEquals("2.00,EUR", result.getGap());
        assertEquals("22.00,EUR", result.getEnd());
        assertEquals(6, result.getBefore());
        assertEquals(3, result.getAfter());
        assertEquals(6, result.getBetween());

        @SuppressWarnings({"unchecked"})
        List<RangeFacet.Count> counts = result.getCounts();
        if (use_mincount) {
          assertEquals(2, counts.size());
          for (int i = 0; i < 2; i++) {
            RangeFacet.Count bucket = counts.get(i);
            assertEquals((12 + (i * 2)) + ".00,EUR", bucket.getValue());
            assertEquals("bucket #" + i, 3, bucket.getCount());
          }
        } else {
          assertEquals(7, counts.size());
          for (int i = 0; i < 7; i++) {
            RangeFacet.Count bucket = counts.get(i);
            assertEquals((8 + (i * 2)) + ".00,EUR", bucket.getValue());
            assertEquals("bucket #" + i, (i == 2 || i == 3) ? 3 : 0, bucket.getCount());
          }
        }
      } catch (AssertionError | RuntimeException ae) {
        throw new AssertionError(solrQuery + " -> " + rsp + " ===> " + ae.getMessage(), ae);
      }
    }
  }

  public void testJsonFacetRangeOfAsymmetricRates() throws Exception {
    // json.facet: exchange rates relative to EUR (same as testFacetRangeOfAsymmetricRates)
    //
    // because of the asymmetric exchange rate, the counts for these buckets will be different
    // from if we just converted the EUR values to USD
    for (boolean use_mincount : Arrays.asList(true, false)) {
      final SolrQuery solrQuery =
          new SolrQuery(
              "q",
              "*:*",
              "rows",
              "0",
              "json.facet",
              "{ foo:{ type:range, field:"
                  + FIELD
                  + ", start:'8,EUR', "
                  + "        mincount:"
                  + (use_mincount ? 3 : 0)
                  + ", "
                  + "        gap:'2,EUR', end:'22,EUR', other:all}}");
      final QueryResponse rsp = cluster.getSolrClient(COLLECTION).query(solrQuery);
      try {
        assertEquals(NUM_DOCS, rsp.getResults().getNumFound());

        @SuppressWarnings({"unchecked"})
        final NamedList<Object> foo =
            ((NamedList<NamedList<Object>>) rsp.getResponse().get("facets")).get("foo");

        assertEquals("before", 6L, ((NamedList) foo.get("before")).get("count"));
        assertEquals("after", 3L, ((NamedList) foo.get("after")).get("count"));
        assertEquals("between", 6L, ((NamedList) foo.get("between")).get("count"));

        @SuppressWarnings({"unchecked", "rawtypes"})
        final List<NamedList> buckets = (List<NamedList>) foo.get("buckets");

        if (use_mincount) {
          assertEquals(2, buckets.size());
          for (int i = 0; i < 2; i++) {
            @SuppressWarnings({"rawtypes"})
            NamedList bucket = buckets.get(i);
            assertEquals((12 + (i * 2)) + ".00,EUR", bucket.get("val"));
            assertEquals("bucket #" + i, 3L, bucket.get("count"));
          }
        } else {
          assertEquals(7, buckets.size());
          for (int i = 0; i < 7; i++) {
            @SuppressWarnings({"rawtypes"})
            NamedList bucket = buckets.get(i);
            assertEquals((8 + (i * 2)) + ".00,EUR", bucket.get("val"));
            assertEquals("bucket #" + i, (i == 2 || i == 3) ? 3L : 0L, bucket.get("count"));
          }
        }
      } catch (AssertionError | RuntimeException ae) {
        throw new AssertionError(solrQuery + " -> " + rsp + " ===> " + ae.getMessage(), ae);
      }
    }
  }

  public void testFacetRangeCleanErrorOnMissmatchCurrency() {
    final String expected = "Cannot compare CurrencyValues when their currencies are not equal";
    ignoreException(expected);

    // test to check clean error when start/end have diff currency (facet.range)
    final SolrQuery solrQuery =
        new SolrQuery(
            "q",
            "*:*",
            "rows",
            "0",
            "facet",
            "true",
            "facet.range",
            FIELD,
            "f." + FIELD + ".facet.range.start",
            "0,EUR",
            "f." + FIELD + ".facet.range.gap",
            "10,EUR",
            "f." + FIELD + ".facet.range.end",
            "100,USD");
    final SolrException ex =
        expectThrows(
            SolrException.class,
            () -> {
              final QueryResponse rsp = cluster.getSolrClient(COLLECTION).query(solrQuery);
            });
    assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, ex.code());
    assertTrue(ex.getMessage(), ex.getMessage().contains(expected));
  }

  public void testJsonFacetCleanErrorOnMissmatchCurrency() {
    final String expected = "Cannot compare CurrencyValues when their currencies are not equal";
    ignoreException(expected);

    // test to check clean error when start/end have diff currency (json.facet)
    final SolrQuery solrQuery =
        new SolrQuery(
            "q",
            "*:*",
            "json.facet",
            "{ x:{ type:range, field:"
                + FIELD
                + ", "
                + "      start:'0,EUR', gap:'10,EUR', end:'100,USD' } }");
    final SolrException ex =
        expectThrows(
            SolrException.class,
            () -> {
              final QueryResponse rsp = cluster.getSolrClient(COLLECTION).query(solrQuery);
            });
    assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, ex.code());
    assertTrue(ex.getMessage(), ex.getMessage().contains(expected));
  }

  @Test
  public void testJsonRangeFacetWithSubFacet() throws Exception {

    // range facet, with terms facet nested under using limit=2 w/overrequest disabled
    // filter out the first 5 docs (by id) which should ensure that regardless of sharding:
    //  - x2 being the top term for the 1st range bucket
    //  - x0 being the top term for the 2nd range bucket
    //  - the 2nd term in each bucket may vary based on shard/doc placement, but the count will
    // always be '1'
    // ...and in many cases (based on the shard/doc placement) this will require refinement to
    // backfill the top terms
    final String filter = "id_i1:[" + VALUES.size() + " TO *]";

    // the *facet* results should be the same regardless of whether we filter via fq, or using a
    // domain filter on the top facet
    for (boolean use_domain : Arrays.asList(true, false)) {
      final String domain = use_domain ? "domain: { filter:'" + filter + "'}," : "";

      // both of these options should produce same results since hardened:false is default
      final String end = random().nextBoolean() ? "end:'20,EUR'" : "end:'15,EUR'";

      final SolrQuery solrQuery =
          new SolrQuery(
              "q",
              (use_domain ? "*:*" : filter),
              "rows",
              "0",
              "json.facet",
              "{ bar:{ type:range, field:"
                  + FIELD
                  + ", "
                  + domain
                  + "        start:'0,EUR', gap:'10,EUR', "
                  + end
                  + ", other:all "
                  + "        facet: { foo:{ type:terms, field:x_s, "
                  + "                       refine:true, limit:2, overrequest:0"
                  + " } } } }");
      final QueryResponse rsp = cluster.getSolrClient(COLLECTION).query(solrQuery);
      try {
        // this top level result count sanity check that should vary based on how we are filtering
        // our facets...
        assertEquals(use_domain ? 15 : 10, rsp.getResults().getNumFound());

        @SuppressWarnings({"unchecked"})
        final NamedList<Object> bar =
            ((NamedList<NamedList<Object>>) rsp.getResponse().get("facets")).get("bar");
        @SuppressWarnings({"unchecked"})
        final List<NamedList<Object>> bar_buckets = (List<NamedList<Object>>) bar.get("buckets");
        @SuppressWarnings({"unchecked"})
        final NamedList<Object> before = (NamedList<Object>) bar.get("before");
        @SuppressWarnings({"unchecked"})
        final NamedList<Object> between = (NamedList<Object>) bar.get("between");
        @SuppressWarnings({"unchecked"})
        final NamedList<Object> after = (NamedList<Object>) bar.get("after");

        // sanity check our high level expectations...
        assertEquals("bar num buckets", 2, bar_buckets.size());
        assertEquals("before count", 0L, before.get("count"));
        assertEquals("between count", 8L, between.get("count"));
        assertEquals("after count", 2L, after.get("count"));

        // drill into the various buckets...

        // before should have no subfacets since it's empty...
        assertNull("before has foo???", before.get("foo"));

        // our 2 range buckets & their sub facets...
        for (int i = 0; i < 2; i++) {
          final NamedList<Object> bucket = bar_buckets.get(i);
          assertEquals((i * 10) + ".00,EUR", bucket.get("val"));
          assertEquals("bucket #" + i, 4L, bucket.get("count"));
          @SuppressWarnings({"unchecked"})
          final List<NamedList<Object>> foo_buckets =
              ((NamedList<List<NamedList<Object>>>) bucket.get("foo")).get("buckets");
          assertEquals("bucket #" + i + " foo num buckets", 2, foo_buckets.size());
          assertEquals(
              "bucket #" + i + " foo top term",
              (0 == i ? "x2" : "x0"),
              foo_buckets.get(0).get("val"));
          assertEquals("bucket #" + i + " foo top count", 2L, foo_buckets.get(0).get("count"));
          // NOTE: we can't make any assertions about the 2nd val...
          // our limit + randomized sharding could result in either remaining term being picked.
          // but for either term, the count should be exactly the same...
          assertEquals("bucket #" + i + " foo 2nd count", 1L, foo_buckets.get(1).get("count"));
        }

        { // between...
          @SuppressWarnings({"unchecked"})
          final List<NamedList<Object>> buckets =
              ((NamedList<List<NamedList<Object>>>) between.get("foo")).get("buckets");
          assertEquals("between num buckets", 2, buckets.size());
          // the counts should both be 3, and the term order should break the tie...
          assertEquals("between bucket top", "x0", buckets.get(0).get("val"));
          assertEquals("between bucket top count", 3L, buckets.get(0).get("count"));
          assertEquals("between bucket 2nd", "x2", buckets.get(1).get("val"));
          assertEquals("between bucket 2nd count", 3L, buckets.get(1).get("count"));
        }

        { // after...
          @SuppressWarnings({"unchecked"})
          final List<NamedList<Object>> buckets =
              ((NamedList<List<NamedList<Object>>>) after.get("foo")).get("buckets");
          assertEquals("after num buckets", 2, buckets.size());
          // the counts should both be 1, and the term order should break the tie...
          assertEquals("after bucket top", "x1", buckets.get(0).get("val"));
          assertEquals("after bucket top count", 1L, buckets.get(0).get("count"));
          assertEquals("after bucket 2nd", "x2", buckets.get(1).get("val"));
          assertEquals("after bucket 2nd count", 1L, buckets.get(1).get("count"));
        }

      } catch (AssertionError | RuntimeException ae) {
        throw new AssertionError(solrQuery + " -> " + rsp + " ===> " + ae.getMessage(), ae);
      }
    }
  }

  @Test
  public void testJsonRangeFacetAsSubFacet() throws Exception {

    // limit=1, overrequest=1, with refinement enabled
    // filter out the first 5 docs (by id), which should ensure that 'x2' is the top bucket
    // overall...
    //   ...except in some rare sharding cases where it doesn't make it into the top 2 terms.
    //
    // So the filter also explicitly accepts all 'x2' docs -- ensuring we have enough matches
    // containing that term for it to be enough of a candidate in phase#1, but for many shard
    // arrangements it won't be returned by all shards resulting in refinement being necessary to
    // get the x_s:x2 sub-shard ranges from shard(s) where x_s:x2 is only tied for the (shard local)
    // top term count and would lose the (index order) tiebreaker with x_s:x0 or x_s:x1
    final String filter = "id_i1:[" + VALUES.size() + " TO *] OR x_s:x2";

    // the *facet* results should be the same regardless of whether we filter via fq, or using a
    // domain filter on the top facet
    for (boolean use_domain : Arrays.asList(true, false)) {
      final String domain = use_domain ? "domain: { filter:'" + filter + "'}," : "";
      final SolrQuery solrQuery =
          new SolrQuery(
              "q",
              (use_domain ? "*:*" : filter),
              "rows",
              "0",
              "json.facet",
              "{ foo:{ type:terms, field:x_s, refine:true, limit:1, overrequest:1, "
                  + domain
                  + "        facet: { bar:{ type:range, field:"
                  + FIELD
                  + ", other:all, "
                  + "                       start:'8,EUR', gap:'2,EUR', end:'22,EUR' }} } }");
      final QueryResponse rsp = cluster.getSolrClient(COLLECTION).query(solrQuery);
      try {
        // this top level result count sanity check that should vary based on how we are filtering
        // our facets...
        assertEquals(use_domain ? 15 : 11, rsp.getResults().getNumFound());

        @SuppressWarnings({"unchecked"})
        final NamedList<Object> foo =
            ((NamedList<NamedList<Object>>) rsp.getResponse().get("facets")).get("foo");

        // sanity check...
        // because of the facet limit, foo should only have 1 bucket
        // because of the fq, the val should be "x2" and the count=5
        @SuppressWarnings({"unchecked"})
        final List<NamedList<Object>> foo_buckets = (List<NamedList<Object>>) foo.get("buckets");
        assertEquals(1, foo_buckets.size());
        assertEquals("x2", foo_buckets.get(0).get("val"));
        assertEquals("foo bucket count", 5L, foo_buckets.get(0).get("count"));

        @SuppressWarnings({"unchecked"})
        final NamedList<Object> bar = (NamedList<Object>) foo_buckets.get(0).get("bar");

        // these are the 'x2' specific counts, based on our fq...

        assertEquals("before", 2L, ((NamedList) bar.get("before")).get("count"));
        assertEquals("after", 1L, ((NamedList) bar.get("after")).get("count"));
        assertEquals("between", 2L, ((NamedList) bar.get("between")).get("count"));

        @SuppressWarnings({"unchecked", "rawtypes"})
        final List<NamedList> buckets = (List<NamedList>) bar.get("buckets");
        assertEquals(7, buckets.size());
        for (int i = 0; i < 7; i++) {
          @SuppressWarnings({"rawtypes"})
          NamedList bucket = buckets.get(i);
          assertEquals((8 + (i * 2)) + ".00,EUR", bucket.get("val"));
          // 12,EUR & 15,EUR are the 2 values that align with x2 docs
          assertEquals("bucket #" + i, (i == 2 || i == 3) ? 1L : 0L, bucket.get("count"));
        }
      } catch (AssertionError | RuntimeException ae) {
        throw new AssertionError(solrQuery + " -> " + rsp + " ===> " + ae.getMessage(), ae);
      }
    }
  }
}
